package ee.metadataUtils.impl;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import ee.metadataUtils.PropertyMetadata;
import ee.metadataUtils.PropertyMetadataAdapterFactoryRegistry;
import ee.metadataUtils.PropertyMetadataFactory;
import ee.metadataUtils.PropertyMetadataRegistry;
import ee.metadataUtils.PropertyMetadataType;

/**
 * If an adapter factory returns null as an adapter, the property is not included 
 * 
 * @author EECOLOR
 *
 */
public class PropertyMetadataRegistryImpl implements PropertyMetadataRegistry {

	private String _forbiddenAnnotationName;
	private PropertyMetadataFactory _propertyMetadataFactory;
	
	private Map<Class<?>, Map<Class<?>, Map<String, ?>>> _propertyMetadataAdapters;
	private Map<Class<?>, Map<String, PropertyMetadata>> _propertyMetadata;
	private PropertyMetadataType _propertyMetadataType;
	
	/**
	 * Calls this(propertyMetadataType, "Forbidden", new PropertyMetadataFactoryImpl(adapterFactoryRegistry))
	 * 
	 * @param adapterFactoryRegistry
	 */
	public PropertyMetadataRegistryImpl(PropertyMetadataType propertyMetadataType, PropertyMetadataAdapterFactoryRegistry adapterFactoryRegistry)
	{
		this(propertyMetadataType, "Forbidden", new PropertyMetadataFactoryImpl(adapterFactoryRegistry));
	}
	
	public PropertyMetadataRegistryImpl(PropertyMetadataType propertyMetadataType, String forbiddenAnnotationName, PropertyMetadataFactory propertyMetadataFactory)
	{
		_propertyMetadataType = propertyMetadataType;
		_forbiddenAnnotationName = forbiddenAnnotationName;
		_propertyMetadataFactory = propertyMetadataFactory;
		
		_propertyMetadata = Collections.synchronizedMap(new HashMap<Class<?>, Map<String, PropertyMetadata>>());
		_propertyMetadataAdapters = Collections.synchronizedMap(new HashMap<Class<?>, Map<Class<?>, Map<String, ?>>>());
	}
	
	private Map<String, PropertyMetadata> _getPropertyMetadata(Class<?> type)
	{
		if (!_propertyMetadata.containsKey(type))
		{
			_gatherPropertyMetadata(type);
		}
		
		return _propertyMetadata.get(type);
	}
	
	private <T> Map<String, T> _getPropertyMetadataAdapters(Class<?> type, Class<T> adapterType) throws AdapterNotFoundException
	{
		if (!_propertyMetadataAdapters.containsKey(type) || !_propertyMetadataAdapters.get(type).containsKey(adapterType))
		{
			_gatherPropertyMetadataAdapters(type, adapterType);
		}
		
		@SuppressWarnings("unchecked")
		Map<String, T> result = (Map<String, T>) _propertyMetadataAdapters.get(type).get(adapterType);
		return result;
	}
	
	@Override
	public PropertyMetadata getPropertyMetadata(Class<?> type, String propertyName) {
		return _getPropertyMetadata(type).get(propertyName);
	}

	@Override
	public Collection<PropertyMetadata> getPropertyMetadataCollection(Class<?> type) {
		
		return _getPropertyMetadata(type).values();
	}

	@Override
	public Map<String, PropertyMetadata> getPropertyMetadataMap(Class<?> type) {
		return _getPropertyMetadata(type);
	}

	@Override
	public <T> Collection<T> getPropertyMetadataCollection(Class<?> type, Class<T> adapterType) throws AdapterNotFoundException {
		
		return _getPropertyMetadataAdapters(type, adapterType).values();
	}

	@Override
	public <T> Map<String, T> getPropertyMetadataMap(Class<?> type, Class<T> adapterType) throws AdapterNotFoundException {
		return _getPropertyMetadataAdapters(type, adapterType);
	}

	@Override
	public <T> T getPropertyMetadata(Class<?> type, String propertyName, Class<T> adapterType) throws AdapterNotFoundException {
		return _getPropertyMetadataAdapters(type, adapterType).get(propertyName);
	}

	private synchronized <T> void _gatherPropertyMetadataAdapters(Class<?> type, Class<T> adapterType) throws AdapterNotFoundException
	{
		Map<Class<?>, Map<String, ?>> propertyMetadataAdapters;
		
		if (_propertyMetadataAdapters.containsKey(type))
		{
			propertyMetadataAdapters = _propertyMetadataAdapters.get(type);
		} else
		{
			propertyMetadataAdapters = new HashMap<Class<?>, Map<String,?>>();
			_propertyMetadataAdapters.put(type, propertyMetadataAdapters);
		}
		
		if (propertyMetadataAdapters.containsKey(adapterType))
		{
			return;
		}
		
		Map<String, PropertyMetadata> propertyMetadataMap = _getPropertyMetadata(type);
		Map<String, T> propertyMetadataAdaptersMap = new HashMap<String, T>();
		
		for (Entry<String, PropertyMetadata> entry : propertyMetadataMap.entrySet())
		{
			T adapter = entry.getValue().getAdapter(adapterType);
			
			if (adapter != null)
			{
				propertyMetadataAdaptersMap.put(entry.getKey(), adapter);
			}
		}
		
		propertyMetadataAdapters.put(adapterType, propertyMetadataAdaptersMap);
	}
	
	private synchronized void _gatherPropertyMetadata(Class<?> type)
	{
		if (_propertyMetadata.containsKey(type))
		{
			return;
		}
		
		Class<?> currentType = type;
		Map<String, PropertyMetadata> propertyMetadataMap = new HashMap<String, PropertyMetadata>();
		PropertyMetadata propertyMetadata;
		
		int propertyMetadataValue = _propertyMetadataType.getValue();
		
		if ((propertyMetadataValue & PropertyMetadataType.ACCESSORS.getValue()) > 0)
		{
			BeanInfo beanInfo;
			try {
				if (currentType.isInterface())
				{
					beanInfo = Introspector.getBeanInfo(currentType);
				} else
				{
					beanInfo = Introspector.getBeanInfo(currentType, Object.class);
				}
			} catch (IntrospectionException e) {
				throw new RuntimeException(e);
			}
			
			for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors())
			{
				propertyMetadata = _propertyMetadataFactory.create(propertyDescriptor);
				
				if (isAllowed(propertyMetadata))
				{
					propertyMetadataMap.put(propertyMetadata.getName(), propertyMetadata);
				}
			}
		}
		
		if ((propertyMetadataValue & PropertyMetadataType.FIELDS.getValue()) > 0)
		{
			for (Field field : currentType.getFields())
			{
				if (!Modifier.isStatic(field.getModifiers())) {
					propertyMetadata = _propertyMetadataFactory.create(field);
					
					if (isAllowed(propertyMetadata))
					{
						propertyMetadataMap.put(propertyMetadata.getName(), propertyMetadata);
					}
				}
			}
		}
		
		_propertyMetadata.put(type, propertyMetadataMap);
	}

	protected boolean isAllowed(PropertyMetadata propertyMetadata) {
		for (Annotation annotation : propertyMetadata.getAnnotations())
		{
			if (annotation.annotationType().getSimpleName().equals(_forbiddenAnnotationName))
			{
				return false;
			}
		}
		return true;
	}
}
